#!/bin/sh
# This shows all log entries that are not already covered by
# another ref - i.e. commits that are now accessible from this
# ref that were previously not accessible
# (see generate_update_branch_email for the explanation of this
# command)
# Revision range passed to rev-list differs for new vs. updated
# branches.

# Consider this:
#   1 --- 2 --- O --- X --- 3 --- 4 --- N
#
# O is $oldrev for $refname
# N is $newrev for $refname
# X is a revision pointed to by some other ref, for which we may
#   assume that an email has already been generated.
# In this case we want to issue an email containing only revisions
# 3, 4, and N.  Given (almost) by
#
#  git rev-list N ^O --not --all
#
# The reason for the "almost", is that the "--not --all" will take
# precedence over the "N", and effectively will translate to
#
#  git rev-list N ^O ^X ^N
#
# So, we need to build up the list more carefully.  git rev-parse
# will generate a list of revs that may be fed into git rev-list.
# We can get it to make the "--not --all" part and then filter out
# the "^N" with:
#
#  git rev-parse --not --all | grep -v N
#
# Then, using the --stdin switch to git rev-list we have effectively
# manufactured
#
#  git rev-list N ^O ^X
#
# This leaves a problem when someone else updates the repository
# while this script is running.  Their new value of the ref we're
# working on would be included in the "--not --all" output; and as
# our $newrev would be an ancestor of that commit, it would exclude
# all of our commits.  What we really want is to exclude the current
# value of $refname from the --not list, rather than N itself.  So:
#
#  git rev-parse --not --all | grep -v $(git rev-parse $refname)
#
# Get's us to something pretty safe (apart from the small time
# between refname being read, and git rev-parse running - for that,
# I give up)
#
#
# Next problem, consider this:
#   * --- B --- * --- O ($oldrev)
#          \
#           * --- X --- * --- N ($newrev)
#
# That is to say, there is no guarantee that oldrev is a strict
# subset of newrev (it would have required a --force, but that's
# allowed).  So, we can't simply say rev-list $oldrev..$newrev.
# Instead we find the common base of the two revs and list from
# there.
#
# As above, we need to take into account the presence of X; if
# another branch is already in the repository and points at some of
# the revisions that we are about to output - we don't want them.
# The solution is as before: git rev-parse output filtered.
#
# Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N
#
# Tags pushed into the repository generate nice shortlog emails that
# summarise the commits between them and the previous tag.  However,
# those emails don't include the full commit messages that we output
# for a branch update.  Therefore we still want to output revisions
# that have been output on a tag email.
#
# Luckily, git rev-parse includes just the tool.  Instead of using
# "--all" we use "--branches"; this has the added benefit that
# "remotes/" will be ignored as well.

# List all of the revisions that were removed by this update, in a
# fast-forward update, this list will be empty, because rev-list O
# ^N is empty.  For a non-fast-forward, O ^N is the list of removed
# revisions

change_type=$1
newrev=$2
oldrev=$3
refname=$4
custom_showrev=$5
oldrev_type=$6
log_begin_marker=$7
log_end_marker=$8

fast_forward=""
rev=""
for rev in $(git rev-list $newrev..$oldrev)
do
	revtype=$(git cat-file -t "$rev")
	echo "  discards  $rev ($revtype)"
done
if [ -z "$rev" ]; then
	fast_forward=1
fi

# List all the revisions from baserev to newrev in a kind of
# "table-of-contents"; note this list can include revisions that
# have already had notification emails and is present to show the
# full detail of the change from rolling back the old revision to
# the base revision and then forward to the new revision
for rev in $(git rev-list $oldrev..$newrev)
do
	revtype=$(git cat-file -t "$rev")
	echo "       via  $rev ($revtype)"
done

if [ "$fast_forward" ]; then
	echo "      from  $oldrev ($oldrev_type)"
else
	#  1. Existing revisions were removed.  In this case newrev
	#     is a subset of oldrev - this is the reverse of a
	#     fast-forward, a rewind
	#  2. New revisions were added on top of an old revision,
	#     this is a rewind and addition.

	# (1) certainly happened, (2) possibly.  When (2) hasn't
	# happened, we set a flag to indicate that no log printout
	# is required.

	echo ""

	# Find the common ancestor of the old and new revisions and
	# compare it with newrev
	baserev=$(git merge-base $oldrev $newrev)
	rewind_only=""
	if [ "$baserev" = "$newrev" ]; then
		echo "This update discarded existing revisions and left the branch pointing at"
		echo "a previous point in the repository history."
		echo ""
		echo " * -- * -- N ($newrev)"
		echo "            \\"
		echo "             O -- O -- O ($oldrev)"
		echo ""
		echo "The removed revisions are not necessarilly gone - if another reference"
		echo "still refers to them they will stay in the repository."
		rewind_only=1
	else
		echo "This update added new revisions after undoing existing revisions.  That is"
		echo "to say, the old revision is not a strict subset of the new revision.  This"
		echo "situation occurs when you --force push a change and generate a repository"
		echo "containing something like this:"
		echo ""
		echo " * -- * -- B -- O -- O -- O ($oldrev)"
		echo "            \\"
		echo "             N -- N -- N ($newrev)"
		echo ""
		echo "When this happens we assume that you've already had alert emails for all"
		echo "of the O revisions, and so we here report only the revisions in the N"
		echo "branch from the common base, B."
	fi
fi

echo ""
if [ -z "$rewind_only" ]; then
	echo ""
	echo "$log_begin_marker"
	"$(dirname $0)/post-receive-email-show-new-revision" "$change_type" "$newrev" "$oldrev" "$refname" "$custom_showrev"

	# XXX: Need a way of detecting whether git rev-list actually
	# outputted anything, so that we can issue a "no new
	# revisions added by this update" message

	echo "$log_end_marker"
else
	echo "No new revisions were added by this update."
fi

# The diffstat is shown from the old revision to the new revision.
# This is to show the truth of what happened in this change.
# There's no point showing the stat from the base to the new
# revision because the base is effectively a random revision at this
# point - the user will be interested in what this revision changed
# - including the undoing of previous revisions in the case of
# non-fast-forward updates.
echo ""
echo "Summary of changes:"
git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev